namespace DataFile
{
    using System;
    using System.IO;
    using System.Text;
    
    public delegate double Transform(double x);
    
    public class Table
    {
        int length;
        int columns;
        double[,] table;
        string description;
        
        public Table(int length, int columns, string description)
        {
            this.length = length;
            this.columns = columns;
            table = new double[length, columns];
            this.description = description;
        }
        
        public Table(int length, int columns) : this(length, columns, "")
        {
        }
        
        public int Columns
        {
            get { return columns; }
        }
        
        public int Length
        {
            get { return length; }
        }
        
        public string Description
        {
            get { return description; }
            set { description = value; }
        }
        
        public double this [int i, int j]
        {
            get { return table[i, j]; }
            set { table[i, j] = value; }
        }
        
        public double[,] Array
        {
            get { return table; }
        }
        
        // returns the data in the col'th column as an array
        public double[] Column(int col)
        {
            double[] ret = new double[length];
            for (int i = 0; i < length; i++)
                ret[i] = table[i, col];
            return ret;
        }
        
        public void SetColumn(int colnum, double[] data_col)
        {
            if (data_col.Length >= length)
            {
                for (int i = 0; i < length; i++)
                    table[i, colnum] = data_col[i];
            }
            else // if (data_col.Length < length)
            {
                for (int i = 0; i < data_col.Length; i++)
                    table[i, colnum] = data_col[i];
                for (int i = data_col.Length; i < length; i++)
                    table[i, colnum] = 0.0;
            }
        }
        
        public void Clear()
        {
            for (int i = 0; i < length; i++)
            {
                for (int j = 0; j < columns; j++)
                    table[i, j] = 0.0;
            }
        }
        
        public double X(int i)
        {
            return table[i, 0];
        }
        
        public double Y(int i)
        {
            return table[i, 1];
        }
        
        public double Z(int i)
        {
            return table[i, 2];
        }
        
        public void SetX(int i, double x)
        {
            table[i, 0] = x;
        }
        
        public void SetY(int i, double y)
        {
            table[i, 1] = y;
        }
        
        public void SetZ(int i, double z)
        {
            table[i, 2] = z;
        }
        
        // finds the minimum of the data in the col'th column
        public double Minimum(int col)
        {
            double test = table[0, col];
            for (int i = 1; i < length; i++)
            {
                if (table[i, col] < test)
                    test = table[i, col];
            }
            return test;
        }
        
        // finds the maximum of the data in the col'th column
        public double Maximum(int col)
        {
            double test = table[0, col];
            for (int i = 1; i < length; i++)
            {
                if (table[i, col] > test)
                    test = table[i, col];
            }
            return test;
        }
        
        // super simple interpolation.
        // I think it's reasonable to assume that the data in xcol increases monotonically
        public double Interpolate(int xcol, int ycol, double x)
        {
            if (x > table[length -1, xcol])
                return table[length - 1, ycol];
            if (x < table[0, xcol])
                return table[0, ycol];
            
            int i = 0;
            while (x > table[i, xcol]) i++;
            double m = (table[i, ycol] - table[i-1, ycol]) / (table[i, xcol] - table[i-1, xcol]);
            double b = table[i, ycol] - m * table[i, xcol];
            return m * x + b;
        } 
        
        public double Integrate(int xcol, int ycol, int a, int b)
        {
            double Tn = 0.0;
            
            if (a == b)
            {
                if (a == 0)
                    return table[a, ycol] * (table[a + 1, xcol] - table[a, xcol]);
                else
                    return table[a, ycol] * (table[a, xcol] - table[a - 1, xcol]);
            }

            for (int i = a; i < b; ++i)
            {
                Tn += 0.5 * (table[i + 1, xcol] - table[i, xcol]) * (table[i + 1, ycol] + table[i, ycol]);
            }

            return Tn;
        }
        
        public double Derivative(int xcol, int ycol, int i, int n_smooth)
        {
            int n_start = i - n_smooth / 2;
            int n_end = n_start + n_smooth;
            if (n_start < 0)
                n_start = 0;
            if (n_end > Length - 1)
                n_end = Length - 1;
                
            double xbar = 0.0;
            double ybar = 0.0;
            int count = 0;
            for (int j = n_start; j < n_end; j++)
            {
                xbar += table[j, xcol];
                ybar += table[j, ycol];
                count++;
            }
            xbar /= (double)count;
            ybar /= (double)count;
            
            double ss_xy = 0.0;
            double ss_xx = 0.0;
            for (int j = n_start; j < n_end; j++)
            {
                ss_xy += (table[j, xcol] - xbar) * (table[j, ycol] - ybar);
                ss_xx += (table[j, xcol] - xbar) * (table[j, xcol] - xbar);
            }
            double m = ss_xy / ss_xx;
            
            return m;
        }           
        
        // removes every nth row in the table, where n is set to make the number of rows
        // left not more than num_to_keep
        public void TrimTo(int num_to_keep)
        {
            if (num_to_keep > length) return;
            
            double mod = (int)Math.Ceiling((double)length / (double)num_to_keep);
            int new_length = (int)Math.Ceiling((double)length / (double)mod);
            
            int mod_value = 1;
            if (new_length < length)
                mod_value = (int)Math.Ceiling((double)length / (double)new_length);
                
            int c = 0;
            double[,] new_data = new double[new_length, columns];
            for (int i = 0; i < length; i++)
            {
                if ((i % mod_value) == 0)
                {
                    for (int j = 0; j < columns; j++)
                    {   
                        new_data[c, j] = table[i, j];
                    }
                    c++;
                }
            }
            
            table = new_data;
            length = new_length;
        }
        
        public override string ToString()
        {
            // header only
            string ret = "# NBody.DataSet\n";
            ret += "# Length = " + length + "\n";
            ret += "# Columns = " + columns + "\n";
            ret += "# Description: " + description;
                    
            return ret;
        }  
        
        // hmmm ... I got rid of the "okay to print" thing ... so I probably don't 
        // need the "redo the header part" either ...
        
        public void Print(string file)
        {
            string message = String.Format("Writing DataSet ({0} lines, {1} columns) to file \"{2}\"", this.Length, this.Columns, file);
            Console.Error.WriteLine(message);
            
            FileInfo finfo = new FileInfo(file);
            
            if (finfo.Exists) 
                finfo.Delete();

            using (FileStream fs = finfo.Create())
            {
                UTF8Encoding utf8 = new UTF8Encoding();
                                
                // write temp header
                string temp = "# DataFile.Table\n";
                temp += "# Length = " + length + "\n";
                temp += "# Columns = " + columns + "\n";
                temp += "# Description: " + description + "\n";                
                        
                byte[] header_bytes = utf8.GetBytes(temp);
                fs.Write(header_bytes, 0, header_bytes.Length);
                
                int count = 0;
                for (int i = 0; i < length; i++)
                {
                    string line = "";
                    for (int j = 0; j < columns; j++)
                    {
                        double x = table[i, j];
                        line += String.Format("{0:g6} ", x);
                    }
                    byte[] bytes = utf8.GetBytes(line.Trim() + "\n");
                    fs.Write(bytes, 0, bytes.Length);
                    count++;
                }
                
                // fix header
                string header = "# DataFile.Table\n";
                header += "# Length = " + count + "\n";
                header += "# Columns = " + columns + "\n";
                header += "# Description: " + description;
                int diff = temp.Length - header.Length;
                if (diff > 1)
                {
                    for (int i = 0; i < diff - 1; i++)
                        header += " ";
                }
                header += "\n";
                                
                header_bytes = utf8.GetBytes(header);
                fs.Seek(0, SeekOrigin.Begin);
                fs.Write(header_bytes, 0, header_bytes.Length);
            }
        }
        
        public void Print(string file, Transform transform)
        {
            string message = String.Format("Writing DataSet ({0} lines, {1} columns) to file \"{2}\"", this.Length, this.Columns, file);
            Console.Error.WriteLine(message);
            
            FileInfo finfo = new FileInfo(file);
            
            if (finfo.Exists) 
                finfo.Delete();

            using (FileStream fs = finfo.Create())
            {
                UTF8Encoding utf8 = new UTF8Encoding();
                                
                // write temp header
                string temp = "# DataFile.Table\n";
                temp += "# Length = " + length + "\n";
                temp += "# Columns = " + columns + "\n";
                temp += "# Description: " + description + "\n";                
                        
                byte[] header_bytes = utf8.GetBytes(temp);
                fs.Write(header_bytes, 0, header_bytes.Length);
                
                int count = 0;
                for (int i = 0; i < length; i++)
                {
                    string line = "";
                    for (int j = 0; j < columns; j++)
                    {
                        double x = transform(table[i, j]);
                        line += String.Format("{0:g6} ", x);
                    }
                    byte[] bytes = utf8.GetBytes(line.Trim() + "\n");
                    fs.Write(bytes, 0, bytes.Length);
                    count++;
                }
                
                // fix header
                string header = "# DataFile.Table\n";
                header += "# Length = " + count + "\n";
                header += "# Columns = " + columns + "\n";
                header += "# Description: " + description;
                int diff = temp.Length - header.Length;
                if (diff > 1)
                {
                    for (int i = 0; i < diff - 1; i++)
                        header += " ";
                }
                header += "\n";
                                
                header_bytes = utf8.GetBytes(header);
                fs.Seek(0, SeekOrigin.Begin);
                fs.Write(header_bytes, 0, header_bytes.Length);
            }
        }
        
        public void Print(string file, Transform[] transforms)
        {
            string message = String.Format("Writing DataSet ({0} lines, {1} columns) to file \"{2}\"", this.Length, this.Columns, file);
            Console.Error.WriteLine(message);
            
            FileInfo finfo = new FileInfo(file);
            
            if (finfo.Exists) 
                finfo.Delete();

            using (FileStream fs = finfo.Create())
            {
                UTF8Encoding utf8 = new UTF8Encoding();
                                
                // write temp header
                string temp = "# DataFile.Table\n";
                temp += "# Length = " + length + "\n";
                temp += "# Columns = " + columns + "\n";
                temp += "# Description: " + description + "\n";                
                        
                byte[] header_bytes = utf8.GetBytes(temp);
                fs.Write(header_bytes, 0, header_bytes.Length);
                
                int count = 0;
                for (int i = 0; i < length; i++)
                {
                    string line = "";
                    for (int j = 0; j < columns; j++)
                    {
                        double x = transforms[j](table[i, j]);
                        line += String.Format("{0:g6} ", x);
                    }
                    byte[] bytes = utf8.GetBytes(line.Trim() + "\n");
                    fs.Write(bytes, 0, bytes.Length);
                    count++;
                }
                
                // fix header
                string header = "# DataFile.Table\n";
                header += "# Length = " + count + "\n";
                header += "# Columns = " + columns + "\n";
                header += "# Description: " + description;
                int diff = temp.Length - header.Length;
                if (diff > 1)
                {
                    for (int i = 0; i < diff - 1; i++)
                        header += " ";
                }
                header += "\n";
                                
                header_bytes = utf8.GetBytes(header);
                fs.Seek(0, SeekOrigin.Begin);
                fs.Write(header_bytes, 0, header_bytes.Length);
            }
        }
        
        public static Table Read(string file)
        {
            StreamReader tr = new StreamReader(File.OpenRead(file));
            
            // there are three possible file formats to consider:
            // 1.  The format written by the above Print functions
            // 2.  A basic format consisting of N columns of numbers with no header
            // 3.  The "tipsy" array format, consisting of on column, headed by the number
            //     of lines.
            
            // read first line
            string line = tr.ReadLine().Trim();
            
            int num_cols, num_lines;
            string desc;
            if (line == "# DataFile.Table")
                GetFileInfoFromHeader(tr, out num_cols, out num_lines, out desc);
            else
            {
            	while (line.StartsWith("#")) // commented lines, read next
            		line = tr.ReadLine().Trim();
            		
                string[] words = line.Split(null);
                if (words.Length == 1) // assume tipsy array format
                {
                    num_cols = 1;
                    try
                    {
                        num_lines = Int32.Parse(line);
                    }
                    catch (Exception)
                    {
                        throw new DataFileException("File " + file + " has incorrect format");
                    }
                }
                else // assume just columns of numbers
                {
                    num_cols = 0;
                    for (int i = 0; i < words.Length; i++)
                    {
                        if (words[i] != String.Empty)
                            num_cols++;
                    }
                    num_lines = GetNumLines(tr);
                    // only make it this far if we can seek, so rewind stream
                    tr.BaseStream.Seek(0, SeekOrigin.Begin);
                }
                desc = "";
            }
            
            Table table = new Table(num_lines, num_cols, desc);
            string message = String.Format("Reading DataSet ({0} lines, {1} columns) from file \"{2}\"", table.Length, table.Columns, file);
            Console.Error.WriteLine(message);
            
            for (int i = 0; i < num_lines; i++)
            {
                line = tr.ReadLine().Trim();
                if (!line.StartsWith("#")) // assign 0.0 ... ?
                {
                    string[] words = line.Split(null);
                    int col = 0;
                    for (int j = 0; j < num_cols; j++)
                    {
                        if (words[j] != String.Empty)
                        {
                            table[i, col] = Double.Parse(words[j].Trim());
                            col++;
                        }
                    }
                }
            }
            
            return table;
        }
        
        public static double IdentityTransform(double x)
        {
            return x;
        }
        
        private static void GetFileInfoFromHeader(StreamReader tr, out int num_cols, out int num_lines, out string desc)
        {
            // number of lines
            string[] words = tr.ReadLine().Split(null);
            num_lines = Int32.Parse(words[3]);
            
            // number of columns
            words = tr.ReadLine().Split(null);
            num_cols = Int32.Parse(words[3]);
            
            // description
            desc = tr.ReadLine().Remove(0, 15);
        }
        
        private static int GetNumLines(StreamReader tr)
        {
            int c = 1;
            try
            {
                if (tr.BaseStream.CanSeek)
                {
                    while (tr.Peek() >= 0)
                    {
                        string test = tr.ReadLine().Trim();
                        if (!test.StartsWith("#"))
                        	c++;
                    }
                }
                else
                {
                    throw new DataFileException("Can't seek within this stream");
                }
            }
            catch (Exception)
            {
                throw new DataFileException("Can't convert to stream");
            }
            return c;
        }
    }
}        
